
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>丰禾智农 · 作物推荐 & 产量预测（本地）</title>
  <!-- 使用稳定版 Tailwind CSS -->
  <link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
</head>
<body class="min-h-screen bg-gradient-to-br from-green-50 to-white text-slate-800">
  <div class="max-w-6xl mx-auto p-4 md:p-8">
    <header class="mb-6 flex items-start justify-between gap-4">
      <div>
        <h1 class="text-3xl font-bold">农业大王 · 作物推荐 & 产量预测</h1>
        <p class="text-slate-600">本地运行 · 支持模型上传/激活 ·
          <code class="text-xs px-1 py-0.5 rounded bg-slate-100">
            API: <span id="apiBaseLabel">http://127.0.0.1:18081</span>
          </code>
        </p>
      </div>
      <div class="flex items-center gap-2">
        <input id="apiBase" class="border rounded p-2 w-56 outline-none focus:ring-2 focus:ring-emerald-200 focus:border-emerald-400 transition" placeholder="自定义 API，如 http://127.0.0.1:18081" />
        <button id="saveApiBtn" class="px-3 py-2 rounded bg-slate-700 text-white hover:bg-slate-800 disabled:opacity-60">保存</button>
      </div>
    </header>

    <!-- 模型管理 -->
    <section class="mb-8 p-4 border rounded-xl bg-white shadow-sm">
      <div class="flex items-center justify-between">
        <h2 class="text-xl font-semibold">模型上传 / 切换</h2>
        <button onclick="refreshList()" class="px-3 py-2 rounded bg-slate-700 text-white hover:bg-slate-800 disabled:opacity-60">刷新</button>
      </div>
      <div class="grid md:grid-cols-3 gap-4 mt-4">
        <!-- 上传 -->
        <div class="p-3 border rounded-lg">
          <h3 class="font-medium mb-2">上传模型</h3>
          <label class="text-sm text-slate-600">模型类型</label>
          <select id="kind" class="border rounded p-2 w-full mt-1 outline-none focus:ring-2 focus:ring-emerald-200 focus:border-emerald-400 transition">
            <option value="crop">作物推荐（.pkl/.joblib）</option>
            <option value="yield">产量预测（.pkl/.joblib）</option>
          </select>
          <input id="mfile" type="file" class="mt-2 w-full" />
          <button id="uploadBtn" onclick="uploadModel()" class="px-4 py-2 rounded bg-emerald-600 text-white hover:bg-emerald-700 disabled:opacity-60 mt-2">上传并启用</button>
          <pre id="upmsg" class="text-xs bg-slate-50 border rounded p-2 mt-2 whitespace-pre-wrap"></pre>
        </div>
        <!-- 已存在 -->
        <div class="p-3 border rounded-lg">
          <h3 class="font-medium mb-2">已存在模型</h3>
          <ul id="filelist" class="mt-2 text-sm space-y-2"></ul>
        </div>
        <!-- 当前激活 -->
        <div class="p-3 border rounded-lg">
          <h3 class="font-medium mb-2">当前激活</h3>
          <pre id="active" class="text-sm bg-slate-50 p-2 rounded border min-h-[112px]">加载中...</pre>
        </div>
      </div>
    </section>

    <!-- 环境特征表单 -->
    <section class="mb-8 grid md:grid-cols-2 gap-6">
      <!-- 作物推荐 -->
      <div class="p-4 border rounded-xl bg-white shadow-sm">
        <div class="flex items-center justify-between mb-3">
          <h2 class="text-xl font-semibold">作物推荐</h2>
          <div class="flex items-center gap-2">
            <button class="px-3 py-2 rounded bg-slate-100 hover:bg-slate-200" onclick="fillExample()">填入示例</button>
            <button class="px-3 py-2 rounded bg-slate-100 hover:bg-slate-200" onclick="clearForm()">清空</button>
          </div>
        </div>

        <!-- 土壤营养 -->
        <div class="mb-4">
          <div class="text-sm font-semibold text-slate-700">🧪 土壤营养成分</div>
          <div class="grid grid-cols-2 gap-3 mt-2">
            <label class="flex flex-col text-sm text-slate-600">
              <span>氮含量 (N) <em class="not-italic text-slate-400 text-xs ml-1">kg/亩</em></span>
              <input class="border rounded p-2 outline-none focus:ring-2 focus:ring-emerald-200 focus:border-emerald-400 transition w-full" placeholder="建议 0-140" id="N" inputmode="decimal" />
            </label>
            <label class="flex flex-col text-sm text-slate-600">
              <span>磷含量 (P) <em class="not-italic text-slate-400 text-xs ml-1">kg/亩</em></span>
              <input class="border rounded p-2 outline-none focus:ring-2 focus:ring-emerald-200 focus:border-emerald-400 transition w-full" placeholder="建议 5-145" id="P" inputmode="decimal" />
            </label>
            <label class="flex flex-col text-sm text-slate-600">
              <span>钾含量 (K) <em class="not-italic text-slate-400 text-xs ml-1">kg/亩</em></span>
              <input class="border rounded p-2 outline-none focus:ring-2 focus:ring-emerald-200 focus:border-emerald-400 transition w-full" placeholder="建议 5-205" id="K" inputmode="decimal" />
            </label>
            <label class="flex flex-col text-sm text-slate-600">
              <span>pH 值</span>
              <input class="border rounded p-2 outline-none focus:ring-2 focus:ring-emerald-200 focus:border-emerald-400 transition w-full" placeholder="例如 6.5" id="ph" inputmode="decimal" />
            </label>
          </div>
        </div>

        <!-- 气候环境 -->
        <div class="mb-2">
          <div class="text-sm font-semibold text-slate-700">🌤️ 气候环境条件</div>
          <div class="grid grid-cols-2 gap-3 mt-2">
            <label class="flex flex-col text-sm text-slate-600">
              <span>温度 <em class="not-italic text-slate-400 text-xs ml-1">°C</em></span>
              <input class="border rounded p-2 outline-none focus:ring-2 focus:ring-emerald-200 focus:border-emerald-400 transition w-full" placeholder="例如 25" id="temperature" inputmode="decimal" />
            </label>
            <label class="flex flex-col text-sm text-slate-600">
              <span>湿度 <em class="not-italic text-slate-400 text-xs ml-1">%</em></span>
              <input class="border rounded p-2 outline-none focus:ring-2 focus:ring-emerald-200 focus:border-emerald-400 transition w-full" placeholder="例如 60" id="humidity" inputmode="decimal" />
            </label>
            <label class="flex flex-col text-sm text-slate-600 col-span-2">
              <span>降雨量 <em class="not-italic text-slate-400 text-xs ml-1">mm</em></span>
              <input class="border rounded p-2 outline-none focus:ring-2 focus:ring-emerald-200 focus:border-emerald-400 transition w-full" placeholder="例如 120" id="rainfall" inputmode="decimal" />
            </label>
          </div>
        </div>

        <div class="mt-3 flex items-center gap-2">
          <button id="cropBtn" onclick="predictCrop()" class="px-4 py-2 rounded bg-emerald-600 text-white hover:bg-emerald-700 disabled:opacity-60">推荐作物</button>
          <span id="formMsg" class="text-sm text-rose-600"></span>
        </div>

        <div id="crop_out_wrap" class="mt-3 hidden">
          <div class="text-sm font-semibold text-slate-700">推荐结果</div>
          <div id="crop_cards" class="mt-2 grid grid-cols-1 gap-2"></div>
          <details class="mt-2">
            <summary class="cursor-pointer text-slate-600 text-sm">查看原始响应 JSON</summary>
            <pre id="crop_out" class="mt-2 text-xs bg-slate-50 p-2 rounded border"></pre>
          </details>
        </div>
      </div>

      <!-- 产量预测 -->
      <div class="p-4 border rounded-xl bg-white shadow-sm">
        <h2 class="text-xl font-semibold mb-1">产量预测</h2>
        <p class="text-sm text-slate-600 mb-3">默认使用左侧同一组环境特征</p>
        <div class="flex items-center gap-2">
          <button id="yieldBtn" onclick="predictYield()" class="px-4 py-2 rounded bg-indigo-600 text-white hover:bg-indigo-700 disabled:opacity-60">预测产量</button>
          <span id="yieldMsg" class="text-sm text-rose-600"></span>
        </div>

        <div id="yield_out_wrap" class="mt-3 hidden">
          <div class="text-sm font-semibold text-slate-700">预测结果</div>
          <div id="yield_cards" class="mt-2 grid grid-cols-1 gap-2"></div>
          <details class="mt-2">
            <summary class="cursor-pointer text-slate-600 text-sm">查看原始响应 JSON</summary>
            <pre id="yield_out" class="mt-2 text-xs bg-slate-50 p-2 rounded border"></pre>
          </details>
        </div>
      </div>
    </section>

    <footer class="text-center text-xs text-slate-500">FastAPI @ <span id="apiFoot">127.0.0.1:18081</span></footer>
  </div>

  <!-- ✅ 只有一个完整的脚本 -->
  <script>
    // ========= 基础 =========
    const g = id => document.getElementById(id);
    const DEFAULT_API = "http://127.0.0.1:18081";
    let API = localStorage.getItem("agri_api_base") || DEFAULT_API;
    g("apiBase").value = API;
    g("apiBaseLabel").textContent = API;
    g("apiFoot").textContent = new URL(API).host;

    g("saveApiBtn").addEventListener("click", () => {
      const v = g("apiBase").value.trim();
      if (!v) return;
      API = v;
      localStorage.setItem("agri_api_base", API);
      g("apiBaseLabel").textContent = API;
      try { g("apiFoot").textContent = new URL(API).host; } catch {}
      toast("API 已保存");
      refreshList();
    });

    // ========= 表单工具 =========
    const FIELD_IDS = ["N","P","K","temperature","humidity","ph","rainfall"];
    function feats(){
      const o = {};
      for (const k of FIELD_IDS) {
        const raw = g(k).value.trim();
        o[k] = raw === "" ? null : Number(raw);
      }
      return o;
    }
    function isNumber(v){ return typeof v === "number" && !Number.isNaN(v); }
    function validate(){
      const f = feats();
      const errs = [];
      if (!isNumber(f.N) || f.N < 0 || f.N > 140) errs.push("N 建议在 0–140");
      if (!isNumber(f.P) || f.P < 5 || f.P > 145) errs.push("P 建议在 5–145");
      if (!isNumber(f.K) || f.K < 5 || f.K > 205) errs.push("K 建议在 5–205");
      if (!isNumber(f.ph) || f.ph <= 0 || f.ph > 14) errs.push("pH 建议在 0–14");
      if (!isNumber(f.temperature)) errs.push("温度必须为数字");
      if (!isNumber(f.humidity) || f.humidity < 0 || f.humidity > 100) errs.push("湿度 0–100");
      if (!isNumber(f.rainfall) || f.rainfall < 0) errs.push("降雨量需 ≥ 0");
      return { ok: errs.length === 0, errs, f };
    }
    function fillExample(){
      const demo = { N:20, P:10, K:30, temperature:25, humidity:60, ph:6.5, rainfall:120 };
      for (const k of FIELD_IDS) g(k).value = demo[k];
      saveForm();
    }
    function clearForm(){
      for (const k of FIELD_IDS) g(k).value = "";
      saveForm();
    }
    function saveForm(){ localStorage.setItem("agri_form", JSON.stringify(feats())); }
    function loadForm(){
      try{
        const m = JSON.parse(localStorage.getItem("agri_form")||"{}");
        FIELD_IDS.forEach(k => { if (m[k] !== undefined && m[k] !== null) g(k).value = m[k]; });
      }catch{}
    }
    FIELD_IDS.forEach(id => g(id).addEventListener("change", saveForm));
    loadForm();

    // ========= 统一 fetch =========
    async function doFetch(url, opts = {}, timeoutMs = 15000){
      const ctrl = new AbortController();
      const t = setTimeout(() => ctrl.abort(), timeoutMs);
      try{
        const r = await fetch(url, { ...opts, signal: ctrl.signal });
        const ct = r.headers.get("content-type") || "";
        const data = ct.includes("application/json") ? await r.json() : await r.text();
        if (!r.ok) throw new Error(typeof data === "string" ? data : JSON.stringify(data));
        return data;
      }finally{ clearTimeout(t); }
    }

    // ========= Toast =========
    function toast(msg){
      const el = document.createElement("div");
      el.className = "fixed left-1/2 -translate-x-1/2 top-4 px-3 py-2 rounded bg-slate-900 text-white text-sm shadow z-50";
      el.textContent = msg;
      document.body.appendChild(el);
      setTimeout(()=> el.remove(), 1800);
    }

    // ========= 模型相关 =========
    async function uploadModel(){
      const btn = g("uploadBtn");
      btn.disabled = true; btn.textContent = "上传中…";
      g("upmsg").textContent = "";
      try{
        const f = g("mfile").files[0];
        if(!f){ g("upmsg").innerText="请选择文件"; return; }
        const fd = new FormData();
        fd.append("kind", g("kind").value);
        fd.append("file", f, f.name);
        const j = await doFetch(API + "/admin/upload_model", { method:"POST", body: fd });
        g("upmsg").innerText = JSON.stringify(j, null, 2);
        toast("上传完成");
        refreshList();
      }catch(e){
        g("upmsg").innerText = "上传失败：\n" + (e.message || e);
      }finally{
        btn.disabled = false; btn.textContent = "上传并启用";
      }
    }

    async function refreshList(){
      const ul = g("filelist");
      ul.innerHTML = `<li class="text-slate-500">加载中…</li>`;
      try{
        const j = await doFetch(API + "/admin/list_models");
        g("active").innerText = JSON.stringify(j.active, null, 2);
        ul.innerHTML = "";
        (j.files||[]).forEach(name=>{
          const li = document.createElement("li");
          li.className = "flex items-center justify-between gap-2";
          li.innerHTML = `
            <div class="truncate"><code class="px-1 rounded bg-slate-100">${name}</code></div>
            <div class="flex items-center gap-1 shrink-0">
              <select class="border rounded p-1 h-8">
                <option value="crop">crop</option>
                <option value="yield">yield</option>
              </select>
              <button class="px-3 py-1 rounded bg-slate-700 text-white hover:bg-slate-800 disabled:opacity-60"
                onclick="setActive(this.previousElementSibling.value,'${name}')">设为激活</button>
            </div>`;
          ul.appendChild(li);
        });
      }catch(e){
                ul.innerHTML = `<li class="text-rose-600">获取失败：${e.message||e}</li>`;
      }
    }

    async function setActive(kind, filename){
      try{
        const fd = new FormData();
        fd.append("kind", kind); fd.append("filename", filename);
        const j = await doFetch(API + "/admin/set_active", { method:"POST", body: fd });
        g("active").innerText = JSON.stringify(j.active || j, null, 2);
        toast("已切换激活模型");
      }catch(e){
        toast("切换失败");
      }
    }

    // ========= 预测：作物推荐 =========
    async function predictCrop(){
      const { ok, errs, f } = validate();
      g("formMsg").textContent = ok ? "" : ("请检查： " + errs.join("；"));
      if (!ok) return;

      const btn = g("cropBtn");
      btn.disabled = true; btn.textContent = "分析中…";
      try{
        const j = await doFetch(API + "/predict/crop",{
          method:"POST", headers:{"Content-Type":"application/json"},
          body: JSON.stringify(f)
        });
        g("crop_out").innerText = JSON.stringify(j, null, 2);
        g("crop_out_wrap").classList.remove("hidden");
        renderCropCards(j);
        toast("推荐完成");
      }catch(e){
        g("crop_out_wrap").classList.add("hidden");
        g("formMsg").textContent = "请求失败：" + (e.message||e);
        toast("推荐失败");
      }finally{
        btn.disabled = false; btn.textContent = "推荐作物";
      }
    }

    function renderCropCards(resp){
      const wrap = g("crop_cards");
      wrap.innerHTML = "";
      let items = [];
      if (Array.isArray(resp)) items = resp;
      else if (resp.top_k) items = resp.top_k;
      else if (resp.label) items = [{ label: resp.label, score: resp.score ?? resp.prob ?? 1 }];
      else if (resp.predictions) items = resp.predictions;

      if (!items || !items.length){
        wrap.innerHTML = `<div class="text-sm text-slate-400 italic">暂无推荐结果</div>`;
        return;
      }
      items.slice(0, 6).forEach(it=>{
        const name = it.name || it.label || it.crop || "未知作物";
        const score = typeof it.score === "number" ? it.score : (typeof it.prob === "number" ? it.prob : null);
        const pct = score!=null ? Math.round(score*100) : null;
        const badge = pct!=null ? `<span class="px-2 py-0.5 rounded-full text-xs font-medium bg-emerald-100 text-emerald-700">${pct}% 匹配</span>` : "";
        const memo = it.reason || it.note || "";
        const li = document.createElement("div");
        li.className = "p-3 border rounded-lg bg-white shadow-sm";
        li.innerHTML = `
          <div class="flex items-center justify-between">
            <div class="text-base font-semibold">${emojiByName(name)}&nbsp;${name}</div>
            ${badge}
          </div>
          ${memo ? `<p class="mt-1 text-sm text-slate-600">${memo}</p>` : ""}
        `;
        wrap.appendChild(li);
      });
    }

    // ========= 预测：产量 =========
    async function predictYield(){
      const { ok, errs, f } = validate();
      g("yieldMsg").textContent = ok ? "" : ("请检查： " + errs.join("；"));
      if (!ok) return;

      const btn = g("yieldBtn");
      btn.disabled = true; btn.textContent = "预测中…";
      try{
        const j = await doFetch(API + "/predict/yield",{
          method:"POST", headers:{"Content-Type":"application/json"},
          body: JSON.stringify(f)
        });
        g("yield_out").innerText = JSON.stringify(j, null, 2);
        g("yield_out_wrap").classList.remove("hidden");
        renderYieldCards(j);
        toast("预测完成");
      }catch(e){
        g("yield_out_wrap").classList.add("hidden");
        g("yieldMsg").textContent = "请求失败：" + (e.message||e);
        toast("预测失败");
      }finally{
        btn.disabled = false; btn.textContent = "预测产量";
      }
    }

    function renderYieldCards(resp){
      const wrap = g("yield_cards");
      wrap.innerHTML = "";
      let items = [];
      if (Array.isArray(resp)) items = resp;
      else if (resp.candidates) items = resp.candidates;
      else if (resp.yield != null) items = [{ crop: resp.crop || "目标作物", yield: resp.yield, unit: resp.unit || "kg/亩" }];

      if (!items || !items.length){
        wrap.innerHTML = `<div class="text-sm text-slate-400 italic">暂无预测结果</div>`;
        return;
      }
      items.slice(0, 6).forEach(it=>{
        const crop = it.crop || it.name || "作物";
        const y = typeof it.yield === "number" ? it.yield : (typeof it.value === "number" ? it.value : null);
        const unit = it.unit || "kg/亩";
        const li = document.createElement("div");
        li.className = "p-3 border rounded-lg bg-white shadow-sm";
        li.innerHTML = `
          <div class="flex items-center justify-between">
            <div class="text-base font-semibold">${emojiByName(crop)}&nbsp;${crop}</div>
            ${y!=null ? `<span class="px-2 py-0.5 rounded-full text-xs font-medium bg-indigo-100 text-indigo-700">${fmt(y)} ${unit}</span>` : ""}
          </div>
          ${it.note ? `<p class="mt-1 text-sm text-slate-600">${it.note}</p>` : ""}
        `;
        wrap.appendChild(li);
      });
    }

    // ========= 小工具 =========
    const EMOJI = { "番茄":"🍅","西红柿":"🍅","黄瓜":"🥒","辣椒":"🌶️","茄子":"🍆","玉米":"🌽","水稻":"🌾","小麦":"🌾","马铃薯":"🥔","大豆":"🫘" };
    function emojiByName(n){ for (const k in EMOJI) if (n.includes(k)) return EMOJI[k]; return "🌱"; }
    function fmt(n){ return Number(n).toLocaleString(undefined,{maximumFractionDigits:2}); }

    // ========= 启动 =========
    refreshList();
  </script>
</body>
</html>
